package com.zjw.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zjw.shortlink.project.common.constant.RedisKeyConstant;
import com.zjw.shortlink.project.common.constant.ShortLinkConstant;
import com.zjw.shortlink.project.common.convention.exception.ClientException;
import com.zjw.shortlink.project.common.convention.exception.ServiceException;
import com.zjw.shortlink.project.common.enums.VailDateTypeEnum;
import com.zjw.shortlink.project.config.GotoDomainWhiteListConfiguration;
import com.zjw.shortlink.project.dao.entity.*;
import com.zjw.shortlink.project.dao.mapper.*;
import com.zjw.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.zjw.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.zjw.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.zjw.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.zjw.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.zjw.shortlink.project.dto.resp.*;
import com.zjw.shortlink.project.mq.producer.DelayShortLinkStatsProducer;
import com.zjw.shortlink.project.mq.producer.ShortLinkStatsSaveProducer;
import com.zjw.shortlink.project.service.ShortLinkService;
import com.zjw.shortlink.project.toolkit.HashUtil;
import com.zjw.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;

@Service
@Slf4j
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;
    private final ShortLinkMapper shortLinkMapper;
    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;
    private final DelayShortLinkStatsProducer delayShortLinkStatsProducer;
    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;

    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocaleAmapKey;

    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParams) {
        verificationWhitelist(requestParams.getOriginUrl());
        String shortLinkSuffix = generateSuffix(requestParams);
        String fullShortURI = StrBuilder.create(requestParams.getDomain()).append("/").append(shortLinkSuffix).toString();
        ShortLinkDO shortLinkDO = BeanUtil.toBean(requestParams, ShortLinkDO.class);
        shortLinkDO.setShortUri(shortLinkSuffix);
        shortLinkDO.setEnableStatus(0);
        shortLinkDO.setFullShortUrl(fullShortURI);
        shortLinkDO.setFavicon(getFavicon(requestParams.getOriginUrl()));
        shortLinkDO.setTotalPv(0);
        shortLinkDO.setTotalUv(0);
        shortLinkDO.setTotalUip(0);
        ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                .gid(requestParams.getGid())
                .fullShortUrl(fullShortURI)
                .build();
        try {
            baseMapper.insert(shortLinkDO);
            shortLinkGotoMapper.insert(shortLinkGotoDO);
        } catch (DuplicateKeyException ex) {
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = new LambdaQueryWrapper<ShortLinkDO>()
                    .eq(ShortLinkDO::getFullShortUrl, fullShortURI);
            ShortLinkDO queryResult = baseMapper.selectOne(queryWrapper);
            if (queryResult != null) {
                log.warn("短链接: {} 重复入库", shortLinkDO.getFullShortUrl());
                throw new ServiceException("短链接生成重复");
            }
        }
        stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortURI),
                requestParams.getOriginUrl(), LinkUtil.getLinkCacheValidDate(requestParams.getValidDate()), TimeUnit.MICROSECONDS);
        shortUriCreateCachePenetrationBloomFilter.add(fullShortURI);
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl(shortLinkDO.getFullShortUrl())
                .gid(shortLinkDO.getGid())
                .originUrl(shortLinkDO.getOriginUrl())
                .build();
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParams) {
        IPage<ShortLinkDO> shortLinkDOIPage = baseMapper.pageLink(requestParams);
        return shortLinkDOIPage.convert(each -> BeanUtil.toBean(each, ShortLinkPageRespDTO.class));
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParams) {
        QueryWrapper<ShortLinkDO> queryWrapper = new QueryWrapper<ShortLinkDO>()
                .select("gid", "count(1) as shortLinkCount")
                .in("gid", requestParams)
                .eq("del_flag", 0)
                .eq("enable_status", 0)
                .eq("del_time", 0L)
                .groupBy("gid");
        List<Map<String, Object>> shortLinkCountDOS = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkCountDOS, ShortLinkGroupCountQueryRespDTO.class);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateShortLink(ShortLinkUpdateReqDTO requestParams) {
        if(StrUtil.isEmpty(requestParams.getOriginGid())
                || StrUtil.isEmpty(requestParams.getFullShortUrl())
                || StrUtil.isEmpty(requestParams.getGid())) {
            throw new ClientException("参数缺失");
        }
        verificationWhitelist(requestParams.getOriginUrl());
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = new LambdaQueryWrapper<ShortLinkDO>()
                .eq(ShortLinkDO::getGid, requestParams.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParams.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        if (hasShortLinkDO == null) {
            throw new ClientException("短链接不存在");
        }
        Integer oldValidDateType = hasShortLinkDO.getValidDateType();
        Date oldValidDate = hasShortLinkDO.getValidDate();
        hasShortLinkDO.setUpdateTime(DateUtil.date());
        LambdaUpdateWrapper<ShortLinkDO> updateWrapper = new LambdaUpdateWrapper<ShortLinkDO>()
                .eq(ShortLinkDO::getGid, requestParams.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParams.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0)
                .set(Objects.equals(requestParams.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null);
        if(StrUtil.isNotEmpty(requestParams.getOriginUrl())) {
            hasShortLinkDO.setOriginUrl(requestParams.getOriginUrl());
            hasShortLinkDO.setFavicon(getFavicon(requestParams.getOriginUrl()));
        }
        if(StrUtil.isNotEmpty(requestParams.getDescribe())) {
            hasShortLinkDO.setDescribe(requestParams.getDescribe());
        }
        if(Objects.equals(requestParams.getValidDateType(), VailDateTypeEnum.CUSTOM.getType()) && requestParams.getValidDate() != null) {
            hasShortLinkDO.setValidDateType(VailDateTypeEnum.CUSTOM.getType());
            hasShortLinkDO.setValidDate(requestParams.getValidDate());
        }
        if(Objects.equals(requestParams.getGid(), hasShortLinkDO.getGid())) {
            baseMapper.update(hasShortLinkDO, updateWrapper);
        } else{
            // 读写锁, 防止短链接更新分组时，统计访问数据异常
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(RedisKeyConstant.LOCK_GID_UPDATE_KEY, requestParams.getFullShortUrl()));
            RLock writeLock = readWriteLock.writeLock();
            if(!writeLock.tryLock()) {
                throw new ClientException("短链接正在更新中，请稍后再试");
            }
            try {
                LambdaUpdateWrapper<ShortLinkDO> linkUpdateWrapper = new LambdaUpdateWrapper<ShortLinkDO>()
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getFullShortUrl, hasShortLinkDO.getFullShortUrl())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                // 之所以增加del_time字段，是因为短链接可能被反复加入同一分组，加入该字段为唯一索引是为了防止更新时唯一索引异常
                ShortLinkDO delShortLinkDO = ShortLinkDO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();
                delShortLinkDO.setDelFlag(1);
                baseMapper.update(delShortLinkDO, linkUpdateWrapper);
                hasShortLinkDO.setGid(requestParams.getGid());
                // 防止主键重复
                hasShortLinkDO.setId(null);
                baseMapper.insert(hasShortLinkDO);
                // 更新短链接所在分组表
                LambdaUpdateWrapper<ShortLinkGotoDO> gotoUpdateWrapper = new LambdaUpdateWrapper<ShortLinkGotoDO>()
                        .eq(ShortLinkGotoDO::getGid, requestParams.getOriginGid())
                        .eq(ShortLinkGotoDO::getFullShortUrl, requestParams.getFullShortUrl());
                ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                        .gid(requestParams.getGid())
                        .build();
                shortLinkGotoMapper.update(shortLinkGotoDO, gotoUpdateWrapper);
            } finally {
                writeLock.unlock();
            }
        }
        // 保证redis缓存一致性
        if(!Objects.equals(oldValidDateType, requestParams.getValidDateType())
                || !Objects.equals(oldValidDate, requestParams.getValidDate())) {
            // 短链接有效期或有效期类型发生改变，则删除短链接跳转缓存
            stringRedisTemplate.delete(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, requestParams.getFullShortUrl()));
            if(oldValidDate != null && oldValidDate.before(new Date())) {
                if(Objects.equals(requestParams.getValidDateType(), VailDateTypeEnum.PERMANENT.getType())
                        || requestParams.getValidDate().after(new Date())) {
                    // 短链接从失效变为有效，则删除空链接缓存
                    stringRedisTemplate.delete(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, requestParams.getFullShortUrl()));
                }
            }
        }
    }

    @Override
    @SneakyThrows
    public void restoreUrl(String shortUrl, HttpServletRequest request, HttpServletResponse response) {
        String serverName = request.getServerName();
        String fullShortUrl = "https://" + serverName + "/" + shortUrl;
        String orginUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotEmpty(orginUrl)) {
            ShortLinkStatsRecordDTO recordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl, recordDTO);
            response.sendRedirect(orginUrl);
            return;
        }
//        if(!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)) {
//            response.sendRedirect("/page/notfound");
//            return;
//        }
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotEmpty(gotoIsNullShortLink)) {
            response.sendRedirect("/page/notfound");
            return;
        }
        RLock lock = redissonClient.getLock(String.format(RedisKeyConstant.LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        try {
            lock.lock();
            // 双重判定锁
            orginUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
            if(StrUtil.isNotBlank(orginUrl)) {
                ShortLinkStatsRecordDTO recordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
                shortLinkStats(fullShortUrl, recordDTO);
                response.sendRedirect(orginUrl);
                return;
            }
            LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = new LambdaQueryWrapper<ShortLinkGotoDO>()
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
            if(shortLinkGotoDO == null) {
                // 严谨来说此处需要进行封控
                response.sendRedirect("/page/notfound");
                stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                return;
            }
            LambdaQueryWrapper<ShortLinkDO> shortLinkQueryWrapper = new LambdaQueryWrapper<ShortLinkDO>()
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(shortLinkQueryWrapper);
            if(shortLinkDO != null) {
                if(shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date())) {
                    response.sendRedirect("/page/notfound");
                    stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                    return;
                }
                stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl),
                        shortLinkDO.getOriginUrl(), LinkUtil.getLinkCacheValidDate(shortLinkDO.getValidDate()), TimeUnit.MICROSECONDS);
                ShortLinkStatsRecordDTO recordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
                shortLinkStats(fullShortUrl, recordDTO);
                response.sendRedirect(shortLinkDO.getOriginUrl());
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParams) {
        List<String> originUrls = requestParams.getOriginUrls();
        List<String> describes = requestParams.getDescribes();
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        for(int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParams, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setDomain(createShortLinkDefaultDomain);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                ShortLinkCreateRespDTO shortLinkDO =createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .describe(describes.get(i))
                        .fullShortUrl(shortLinkDO.getFullShortUrl())
                        .originUrl(shortLinkDO.getOriginUrl())
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    public ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, HttpServletRequest request,HttpServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        AtomicReference<String> userFlag = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            userFlag.set(UUID.randomUUID().toString());
            Cookie cookie = new Cookie("uv", userFlag.get());
            cookie.setMaxAge(60 * 60 * 24 * 30);
            cookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.lastIndexOf("/"), fullShortUrl.length()));
            response.addCookie(cookie);
            uvFirstFlag.set(true);
        };
        Cookie[] cookies = request.getCookies();
        if(ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(cookie -> Objects.equals(cookie.getName(), "uv"))
                    .findFirst()
                    .ifPresentOrElse(cookie -> {
                        userFlag.set(cookie.getValue());
                        Long addUv = stringRedisTemplate.opsForSet().add(String.format(RedisKeyConstant.SHORT_LINK_STATS_UV_KEY, fullShortUrl), userFlag.get());
                        uvFirstFlag.set(addUv != null && addUv > 0L);
                    }, addResponseCookieTask);
        } else{
            addResponseCookieTask.run();
        }
        String remoteAddr = LinkUtil.getActualIp(request);
        Long addUip = stringRedisTemplate.opsForSet().add(String.format(RedisKeyConstant.SHORT_LINK_STATS_UIP_KEY, fullShortUrl), remoteAddr);
        AtomicBoolean uipFirstFlag = new AtomicBoolean(addUip != null && addUip > 0L);
        String os = LinkUtil.getOs(request);
        String browser = LinkUtil.getBrowser(request);
        String device = LinkUtil.getDevice(request);
        String network = LinkUtil.getNetwork(request);
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .uv(userFlag.get())
                .uipFirstFlag(uipFirstFlag.get())
                .uvFirstFlag(uvFirstFlag.get())
                .build();
    }

    @Override
    public void shortLinkStats(String fullShortUrl, ShortLinkStatsRecordDTO recordDTO) {
        Map<String, String> messageMap = new HashMap<>();
        messageMap.put("fullShortUrl", fullShortUrl);
        messageMap.put("record", JSON.toJSONString(recordDTO));
        shortLinkStatsSaveProducer.sendMessage(messageMap);
    }

    private void shortLinkStats(String fullShortUrl, HttpServletRequest request, HttpServletResponse response) {
        Week week = DateUtil.dayOfWeekEnum(new Date());
        int weekValue = week.getIso8601Value();
        int hour = DateUtil.hour(new Date(), true);
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        AtomicReference<String> userFlag = new AtomicReference<>();
        Cookie[] cookies = request.getCookies();
        Runnable addResponseCookieTask = () -> {
            // 第一次访问短链接系统，生成一个用户标识
            Cookie cookie = new Cookie("uv", UUID.randomUUID().toString());
            cookie.setMaxAge(60 * 60 * 24 * 30);
            cookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.lastIndexOf("/"), fullShortUrl.length()));
            response.addCookie(cookie);
            userFlag.set(cookie.getValue());
            Long addFlag = stringRedisTemplate.opsForSet().add(RedisKeyConstant.SHORT_LINK_STATS_UV_KEY + fullShortUrl, cookie.getValue());
            uvFirstFlag.set(addFlag != null && addFlag > 0L);
        };
        if(ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        // 尝试将用户标识插入短链接的访问用户缓存中 -> 短链接,set(访问用户集合)
                        userFlag.set(each);
                        Long addFlag = stringRedisTemplate.opsForSet().add(RedisKeyConstant.SHORT_LINK_STATS_UV_KEY + fullShortUrl, each);
                        uvFirstFlag.set(addFlag != null && addFlag > 0L);
                    }, addResponseCookieTask);
        } else {
            addResponseCookieTask.run();
        }
        String actualIp = LinkUtil.getActualIp(request);
        Long uIpAddFlag = stringRedisTemplate.opsForSet().add(RedisKeyConstant.SHORT_LINK_STATS_UIP_KEY + fullShortUrl, actualIp);
        boolean uIpFirstFlag = uIpAddFlag != null && uIpAddFlag > 0L;
        LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                .pv(1)
                .uip(uIpFirstFlag ? 1 : 0)
                .uv(uvFirstFlag.get() ? 1 : 0)
                .hour(hour)
                .weekday(weekValue)
                .fullShortUrl(fullShortUrl)
                .date(new Date())
                .build();
        linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);
        Map<String, Object> localeParamMap = new HashMap<>();
        localeParamMap.put("key", statsLocaleAmapKey);
        localeParamMap.put("ip", actualIp);
        String localeStr = HttpUtil.get(ShortLinkConstant.AMAP_REMOTE_URL, localeParamMap);
        JSONObject localeResult = JSON.parseObject(localeStr);
        String infoCode = localeResult.getString("infocode");
        String actualProvince = "未知";
        String actualCity = "未知";
        if(StrUtil.isNotBlank(infoCode) && Objects.equals(infoCode, "10000")) {
            String province = localeResult.getString("province");
            boolean unkonownFlag = StrUtil.equals(province, "[]");
            actualProvince = unkonownFlag ? actualProvince : province;
            actualCity = unkonownFlag ? actualCity : localeResult.getString("city");
            LinkLocaleStatsDO localeStatsDO = LinkLocaleStatsDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .province(actualProvince)
                    .city(actualCity)
                    .adcode(localeResult.getString("adcode"))
                    .country("中国")
                    .date(new Date())
                    .cnt(1)
                    .build();
            linkLocaleStatsMapper.shortLinkLocaleState(localeStatsDO);
        }
        String os = LinkUtil.getOs(request);
        LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                .fullShortUrl(fullShortUrl)
                .os(os)
                .cnt(1)
                .date(new Date())
                .build();
        linkOsStatsMapper.shortLinkOsState(linkOsStatsDO);
        String browser = LinkUtil.getBrowser(request);
        LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                .fullShortUrl(fullShortUrl)
                .browser(browser)
                .cnt(1)
                .date(new Date())
                .build();
        linkBrowserStatsMapper.shortLinkBrowserState(linkBrowserStatsDO);
        String device = LinkUtil.getDevice(request);
        LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                .fullShortUrl(fullShortUrl)
                .device(device)
                .cnt(1)
                .date(new Date())
                .build();
        linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);
        String network = LinkUtil.getNetwork(request);
        LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                .fullShortUrl(fullShortUrl)
                .network(network)
                .cnt(1)
                .date(new Date())
                .build();
        linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);
        LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                .fullShortUrl(fullShortUrl)
                .user(userFlag.get())
                .browser(browser)
                .os(os)
                .network(network)
                .device(device)
                .ip(actualIp)
                .locale(String.join("-", "中国", actualProvince, actualCity))
                .build();
        linkAccessLogsMapper.insert(linkAccessLogsDO);
        LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper = new LambdaQueryWrapper<ShortLinkGotoDO>()
                .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
        ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
        String gid = shortLinkGotoDO.getGid();
        shortLinkMapper.incrementStats(gid, fullShortUrl, 1, uvFirstFlag.get() ? 1 : 0, uIpFirstFlag ? 1 : 0);
        LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
                .todayPv(1)
                .todayUv(uvFirstFlag.get() ? 1 : 0)
                .todayUip(uIpFirstFlag ? 1 : 0)
                .fullShortUrl(fullShortUrl)
                .date(new Date())
                .build();
        linkStatsTodayMapper.shortLinkTodayState(linkStatsTodayDO);
    }

    private String generateSuffix(ShortLinkCreateReqDTO requestParams) {
        String originUrl = requestParams.getOriginUrl();
        String shortURL;
        int retryCount = 0;
        while(true) {
            if(retryCount > 10) {
                throw new ServiceException("短链接生成过于频繁");
            }
            shortURL = HashUtil.hashToBase62(originUrl + UUID.randomUUID());
            if(!shortUriCreateCachePenetrationBloomFilter.contains(requestParams.getDomain() + "/" + shortURL)) {
                break;
            }
            retryCount++;
        }
        return shortURL;
    }

    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }

    public void verificationWhitelist(String orginUrl) {
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        if(enable == null || !enable) {
            return;
        }
        String domain = LinkUtil.extractDomain(orginUrl);
        if(StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接填写错误");
        }
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        if(!details.contains(domain)) {
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }
}
